<?php

class DBU_Db_Wrapper {

	protected $adapter;
	protected $connection		= false;
	protected $db_prefix		= false;

	protected $profiler;

	protected $operation		= false;

	protected $query			= false;
	protected $result   		= false;
	protected $activeTable		= false;

	protected $select			= array();
	protected $distinct 		= false;
	protected $insert			= array();
    protected $onDuplicate      = array();
	protected $update			= array();
	protected $from 			= array();
	protected $into 			= array();
	protected $join				= array();
	protected $where			= array();
	protected $group			= array();
	protected $having			= array();
	protected $order			= false;
	protected $limit			= false;


	/**
	 * Instantiates database engine adapter
	 *
	 * @param string $adapter
	 * @param string $db_prefix
	 */
	public function __construct($adapter = false, $db_prefix = false) {
		if ($adapter) {
            $adapter = 'DBU_Db_Adapters_' . ucfirst($adapter);
			$this->adapter = new $adapter();
		} else {
			$this->adapter = new DBU_Db_Adapters_MySQL();
		}

		$this->db_prefix = $db_prefix ? $db_prefix : false;
	}


	/**
	 * Overloads select, from, where, insert, replace, update, order, select_*, delete and inserts corresponding entries in $this->select, $this->where, etc.
	 *
	 * @param string $name
	 * @param string $arguments
	 * @return self
	 */
	public function __call($name, $arguments) {
		if (in_array($name, array('select', 'from', 'where', 'having', 'order'))) {
			if ($arguments) {
				foreach ($arguments as $argument) {
					if ($argument !== null) {
						$this->{$name}[] = $argument;	
					}					
				}
			}
		} elseif (in_array($name, array('insert', 'replace', 'update'))) {
			if ($arguments) {
				if ($name == 'replace') {
					$this->insert = $arguments[0];
				} else {
					$this->{$name} = $arguments[0];
				}
			}
		} elseif (strstr($name, 'select_')) {
			// SELECT_MAX / MIN / AVG / SUM / COUNT

			$operation = strtoupper(str_replace('select_', '', $name));

			$input = reset($arguments);

			if (is_array($input)) {
				$input[] = false;
			} else {
				$input = array($input, false, false);
			}

			$input[0] = $operation . '(' . $this->protect($input[0]) . ')';

			return $this->select($input);

		} elseif ($name == 'delete') {
			$this->activeTable = $arguments[0];
		} else {
			trigger_error('Method [' . $name . '] is not defined', E_USER_ERROR);
		}

		if (in_array($name, array('select', 'insert', 'replace', 'update', 'delete'))) {
			if ($this->operation === false) {
				$this->operation = $name;
			}
		}

		return $this;
	}


	/**
	 * Connects to SQL server
	 *
	 * @param string $host
	 * @param string $user
	 * @param string $password
	 * @param string $database
	 * @return DBU_Db_Wrapper
	 */
	public function connect($host, $user, $password, $database) {
		if (!$this->connection) {
			try {
				$this->connection = $this->adapter->connect($host, $user, $password, $database);
			} catch (DBU_Db_Exception $e) {
				$e->log();
			}
		}
		return $this;
	}


	/**
	 * Sets the DISTINCT flag then passes the arguments to $this->select();
	 *
	 * @return $this->select()
	 */
	public function selectDistinct() {
		$this->distinct = true;
		$args = func_get_args();
		return call_user_func_array(array($this, 'select'), $args);
	}


	/**
	 * Inserts FULLTEXT statement into $this->select and $this->where
	 *
	 * @param mixed $fields
	 * @param string $against
	 * @param string $alias
	 * @param boolean $boolean_mode
	 * @return DBU_Db_Wrapper
	 */
	public function match($fields, $against, $alias, $boolean_mode = true) {
		if (!is_array($fields)) {
			$fields = array();
		}

		$match = array();
		foreach ($fields as $itemField) {
			if (!is_array($itemField)) {
				$itemField = array($itemField);

				$field		= isset($itemField[0]) ? $itemField[0] : false;
				$protected	= isset($itemField[1]) ? $itemField[1] : true;

				$match[] = $protected ? $this->protect($field) : $field;
			}
		}
		$match = "MATCH(" . implode(",", $match) .") AGAINST ('". $against ."'". ($boolean_mode ? " IN BOOLEAN MODE" : "") .")";

		return $this->select(array($match, $alias, false))->where(array($match));
	}


	public function insertInto($table) {
		$this->activeTable = $table;
		$this->operation = 'insertInto';
		return $this;
	}


    public function onDuplicate($update) {
        if ($this->operation == 'insert') {
            $this->onDuplicate = $update;
            return $this;
        } else {
            trigger_error('Operation must be insert for onDuplicate', E_USER_ERROR);
        }
    }



	/**
	 * Mark active table for INSERT operation
	 *
	 * @param string $table
	 * @return DBU_Db_Wrapper
	 */
	public function into($table) {
		$this->activeTable = $table;
		return $this;
	}


	/**
	 * Inserts JOIN entry for the last table inserted by $this->from()
	 *
	 * @param mixed $table the table to be joined, given as simple string or name - alias pair
	 * @param string $on
	 * @param string $type SQL join type (INNER, OUTER, LEFT INNER, etc.)
	 * @return DBU_Db_Wrapper
	 */
	public function join($table, $on, $type = '') {
		$lastTable = end($this->from);

		if (!$lastTable) {
			trigger_error('No previous table to JOIN', E_USER_ERROR);
		}

		$lastTable = $lastTable[0];

		$this->join[$lastTable][] = array($table, $on, $type);

		return $this;
	}


	/**
	 * Sets the group paramater for the query
	 *
	 * @param array $fields
	 * @param boolean $rollup suport for modifier WITH ROLLUP
	 * @return DBU_Db_Wrapper
	 */
	public function group($fields, $rollup = false) {
		$this->group['fields'] 	= $fields;
		$this->group['rollup']	= $rollup;
		return $this;
	}


	public function limit($start, $offset = false) {
		$this->limit = $start;
		if ($offset) {
			$this->limit .= ',' . $offset;
		}
		return $this;
	}


	/**
	 * Compiles and executes current query
	 *
	 * @return DBU_Db_Wrapper
	 */
	public function go() {
		if ($this->operation) {
			$method = 'compile' . ucfirst($this->operation);
			$this->query = $this->$method();
		} else {
			trigger_error('No operation specified');
		}
		return $this->execute($this->query);
	}


	/**
	 * Executes SQL query
	 *
	 * @param string $sql
	 * @return DBU_Db_Result
	 */
	public function execute($sql) {
		$this->clearData();
		try {
			$query   = $this->adapter->execute($sql);
			$result  = new DBU_Db_Result($query, $this->adapter);

			return $result;
		} catch (DBU_Db_Exception $e) {
			$e->log();
		}
	}


	/**
	 * Joins together SELECT, FROM, WHERE, HAVING, ORDER, and LIMIT parts of SQL query
	 *
	 * @return string
	 */
	private function compileSelect() {
		$select = $this->parseSelect();
		$from 	= $this->parseFrom();

		$where 	= $this->parseWhere();
		$group 	= $this->parseGroup();
		$having	= $this->parseHaving();

		$order 	= $this->parseOrder();

		$query 	= 'SELECT ' . ($this->distinct ? 'DISTINCT ' : '') . $select . ' FROM ' . $from;

		if (!empty($where)) {
			$query .= ' WHERE '. $where;
		}

		if (!empty($group)) {
			$query .= ' GROUP BY '. $group;
		}

		if (!empty($having)) {
			$query .= ' HAVING '. $having;
		}

		if (!empty($order)) {
			$query .= ' ORDER BY '. $order;
		}

		if (!empty($this->limit)) {
			$query .= ' LIMIT '. $this->limit;
		}

		return $query;
	}


	/**
	 * Parses SELECT entries
	 *
	 * @return string
	 */
	private function parseSelect() {
		if (!is_array($this->select) OR count($this->select) < 1) {
			return '*';
		} else {
			$selectParts = array();

			foreach ($this->select as $itemSelect) {
				if (!is_array($itemSelect)) {
					$itemSelect = array($itemSelect);
				}

				$field 		= isset($itemSelect[0]) ? $itemSelect[0] : false;
				$alias 		= isset($itemSelect[1]) ? $itemSelect[1] : false;
				$protected 	= isset($itemSelect[2]) ? $itemSelect[2] : true;

				$selectParts[] = ($protected ? $this->protect($field) : $field) . (!empty($alias) ? ' AS ' . $this->protect($alias) : '');
			}

			return implode(', ', $selectParts);
		}
	}

	/**
	 * Parses FROM entries
	 *
	 * @return string
	 */
	private function parseFrom() {
		if (empty($this->from)) {
			trigger_error('Table for SELECT is not defined', E_USER_ERROR);
		} else {
			$fromParts = array();

			foreach ($this->from as $key => $itemFrom) {
				if (!is_array($itemFrom)) {
					$itemFrom = array($itemFrom);
				}

				$table = isset($itemFrom[0]) ? $itemFrom[0] : false;
				$alias = isset($itemFrom[1]) ? $itemFrom[1] : false;

				$fromParts[$key] = $this->protect($this->tableName($table)).' AS '.$this->protect((!empty($alias) ? $alias : $table)) . $this->parseJoin($table);
			}

			return implode(', ', $fromParts);
		}
	}

	/**
	 * Parses JOIN entries for a given table
	 * Concatenates $this->join entries for input table
	 *
	 * @param string $table table to build JOIN statement for
	 * @return string
	 */
	private function parseJoin($table) {
		$result = '';

		if ($this->join[$table]) {
			foreach ($this->join[$table] as $join) {
				if (!is_array($join[0])) {
					$join[0] = array($join[0]);
				}

				$joinTable 	= isset($join[0][0]) ? $this->tableName($join[0][0]) : false;
				$joinAlias 	= isset($join[0][1]) ? $join[0][1] : false;
				$joinOn		= isset($join[1]) ? $join[1] : false;

				if (!is_array($joinOn)) {
					$joinOn = array($joinOn);
				}

				$joinType	= isset($join[2]) ? $join[2] : '';

				$result .= ($joinType ? ' '.strtoupper($joinType) : '') . ' JOIN ' . $this->protect($joinTable) . (!empty($joinAlias) ? ' AS '.$this->protect($joinAlias) : '').' ON ';

				$result .= $this->protect($table.'.'.$joinOn[0]).' = '.$this->protect($joinTable.'.'.$joinOn[1]);
			}
		}

		return $result;
	}


	/**
	 * Parses GROUP entries
	 *
	 * @uses $this->group['fields'] array with elements to group by
	 * @return string
	 */
	private function parseGroup() {
		if (!is_array($this->group['fields'])) {
			return false;
		} else {
			$groupFields = array();

			foreach ($this->group['fields'] as $field) {
				$field 	= is_array($field) ? $field : array($field);
				$column = isset($field[0]) ? $field[0] : false;
				$type 	= isset($field[1]) ? $field[1] : '';

				$groupFields[] = $this->protect($column).($type ? ' '.strtoupper($type) : '');
			}

			$group = implode(', ', $groupFields);

			if ($this->group['rollup']) {
				$group .= ' WITH ROLLUP';
			}

			return $group;
		}
	}


	private function parseWhere() {
		return $this->parseCondition($this->where);
	}


	private function parseHaving() {
		return $this->parseCondition($this->having);
	}


	/**
	 * Parses WHERE & HAVING entries
	 *
	 * @uses $this->where (Array) elements: array($field, $value, $operator, $reference, $type)
	 * @param array $condition $this->where or $this->having
	 *
	 * @internal string  $operator SQL comparison operator: =, <, like, etc
	 * @internal boolean $reference input value given as field reference or simple string
	 * @internal string	$type SQL where condition concatenator (AND, OR, XOR, etc)
	 *
	 * @return string
	 */
	private function parseCondition($condition) {
		if (empty($condition) || !is_array($condition) || count($condition) < 1) {
			return false;
		} else {
			$conditionParts = array();

			foreach ($condition as $itemCondition) {
                if ($itemCondition) {
                    if (!is_array($itemCondition)) {
                        $itemCondition = array($itemCondition);
                    }

                    $field  	= isset($itemCondition[0]) ? $itemCondition[0] : false;
                    $value 		= isset($itemCondition[1]) ? $itemCondition[1] : false;
                    $operator 	= isset($itemCondition[2]) ? $itemCondition[2] : '=';
                    $reference	= isset($itemCondition[3]) ? $itemCondition[3] : false;
                    $type  		= isset($itemCondition[4]) ? $itemCondition[4] : 'AND';
                    $grouping  	= isset($itemCondition[5]) ? $itemCondition[5] : false;

                    $condition = '';

                    if ($value === false) {
                        // $condition = $this->protect($field);
                        $condition .= $field;
                    } else {
                        if (is_array($value)) {
                            $value      = "('". implode("','", array_map(array($this, 'cleanData'), $value)) ."')";
                            $operator   = 'in';
                            
                        } elseif (is_numeric($value)) {
                           
                        } elseif ($reference === true) {
                            $value = $this->protect($value);

                        } else {
                            $value = "'{$this->cleanData($value)}'";
                        }

                        $condition = $this->protect($field).' '.strtoupper($operator).' '.$value;
                    }

                    $preCondition = (!empty ($grouping) && $grouping[0] == '(') ? $grouping : '';
                    $postCondition = (!empty ($grouping) && $grouping[0] == ')') ? $grouping : '';

                    $conditionParts[] = $preCondition . (count($conditionParts) > 0 ? strtoupper($type).' ' : '') . $condition . $postCondition;
                }
			}

			return implode(' ', $conditionParts);
		}
	}

	/**
	 * Parses ORDER BY entries
	 *
	 * @return string
	 */
	private function parseOrder() {
		if (!is_array($this->order) || count($this->order) < 1) {
			return false;
		} else {
			$orderParts = array();

			foreach ($this->order as $itemOrder) {
                if ($itemOrder) {
                    if (!is_array($itemOrder)) {
                        $itemOrder = array($itemOrder);
                    }

                    $column     = isset($itemOrder[0]) ? $itemOrder[0] : false;
                    $type       = isset($itemOrder[1]) ? $itemOrder[1] : '';
                    $protected 	= isset($itemOrder[2]) ? $itemOrder[2] : true;

                    $column = ($protected ? $this->protect($column) : $column).' '.strtoupper($type);

                    $orderParts[] = trim($column);
                }
			}

			return implode(', ', $orderParts);
		}
	}

	/**
	 * Builds INSERT SELECT query
	 *
	 * @return string
	 */
	private function compileInsertInto() {
		if (empty($this->activeTable)) {
			trigger_error('Table for INSERT INTO is not defined', E_USER_ERROR);
		} else {
			return "INSERT INTO " . $this->protect($this->tableName($this->activeTable)) . ' ' . $this->compileSelect();
		}
	}


	/**
	 * Builds INSERT query
	 *
	 * @return string
	 */
	private function compileInsert() {
		if (empty($this->activeTable)) {
			trigger_error('Table for INSERT is not defined', E_USER_ERROR);
		} else {
            $return = "INSERT INTO " . $this->protect($this->tableName($this->activeTable)) . $this->parseInsert();

            if (is_array($this->onDuplicate) && count($this->onDuplicate) >= 1) {
                $this->update = $this->onDuplicate;
                $return .= ' ON DUPLICATE KEY UPDATE ' . $this->parseUpdate();
            }

            return $return;
		}
	}


	/**
	 * Builds REPLACE query
	 *
	 * @return string
	 */
	private function compileReplace() {
		if (empty($this->activeTable)) {
			trigger_error('Table for REPLACE is not defined', E_USER_ERROR);
		} else {
			return "REPLACE INTO " . $this->protect($this->tableName($this->activeTable)) . $this->parseInsert();
		}
	}


	/**
	 * Parses INSERT data
	 *
	 * @return string
	 */
	private function parseInsert() {
		if (!is_array($this->insert) OR count($this->insert) < 1) {
			trigger_error('No data for insert statement', E_USER_ERROR);
		} else {
			$columns = ' (' . implode(',', array_map(array($this, 'protect'), array_keys($this->insert))) . ')';
			$values  = array();

			foreach ($this->insert as $value) {
				if (!is_array($value)) {
					$value = array($value);
				}

				foreach ($value as $key => $insertValue) {
					$values[$key][] = $this->cleanData($insertValue);
				}
			}

			foreach ($values as &$value) {
				$value = "('" . implode("', '", $value) . "')";
			}

			return $columns . ' VALUES ' . implode(', ', $values);
		}
	}


	 /**
	  * Builds UPDATE query
	  *
	  * @return string
	  */
	private function compileUpdate() {
		if (empty($this->activeTable)) {
			trigger_error('Table for UPDATE is not defined', E_USER_ERROR);
		} else {
			$query = "UPDATE " . $this->protect($this->tableName($this->activeTable)) . ' SET ' . $this->parseUpdate() . (!empty($this->where) ? ' WHERE ' . $this->parseWhere() : '') . (!empty($this->limit) ? ' LIMIT ' . $this->limit : '');
			return $query;
			}
	}


	/**
	 * Parses UPDATE data
	 *
	 * @return string
	 */
	private function parseUpdate() {
		if (!is_array($this->update) OR count($this->update) < 1) {
			trigger_error('No data for update statement', E_USER_ERROR);
		} else {
			$fields = array();
			foreach ($this->update as $key => $value) {

				if (!is_array($value)) {
					$value = array($value);
				}

				$updateValue 	= $value[0];
				$protected 		= isset($value[1]) ? $value[1] : false;

				$fields[] = $this->protect($key) . " = " . ($protected ? $this->protect($updateValue) : "'".$this->cleanData($updateValue)."'");
			}
			$query = implode(', ', $fields);
			return $query;
		}
	}


	/**
	* Joins together DELETE, FROM, WHERE, ORDER, and LIMIT parts of SQL query
	*
	* @return string
	*/
	private function compileDelete() {
		$where 	= $this->parseWhere();
		$order 	= $this->parseOrder();

		$query 	= 'DELETE FROM ' . $this->protect($this->tableName($this->activeTable));

		if (!empty($where)) {
			$query .= ' WHERE '. $where;
		}

		if (!empty($order)) {
			$query .= ' ORDER BY '. $order;
		}

		if (!empty($this->limit)) {
			$query .= ' LIMIT '. $this->limit;
		}

		return $query;
	}


	/**
	 * Prefixes table names
	 *
	 * @param string $table
	 * @return string
	 */
	private function tableName($table = '') {
		if ($table == '') {
			trigger_error('[DB::tableName] table name required', E_USER_ERROR);
		}

		return $this->db_prefix . $table;
	}


	/**
	 * Adds backticks to input
	 *
	 * @param string $input
	 * @return string
	 */
	private function protect($input) {
		return str_replace("`*`", "*", '`'.str_replace('.', '`.`', $input).'`');
	}


	/**
	 * Removes backticks from input
	 *
	 * @param string $input
	 * @return string
	 */
	private function cleanProtected($input) {
		return str_replace('`', '', $input);
	}


	/**
	 * Escapes data for safe use in SQL queries
	 *
	 * @param string $data
	 * @return string
	 */
	private function cleanData($data) {
		return $this->adapter->cleanData($data);
	}


	/**
	 * Deletes all the records in a table and resets the count of the auto-incrementing
	 * primary key, where applicable.
	 *
	 * @param 	string $table the table to be truncated
	 * @return 	DBU_Db_Result
	 */
	public function truncate($table) {
		return $this->execute('TRUNCATE TABLE ' . $this->fullTableName($table));
	}


	/**
	 * Resets the current data after execution
	 *
	 */
	public function clearData() {
		$this->operation	= false;

		unset($this->result);

		$this->result   	= false;
		$this->activeTable	= false;

		$this->select 		= array();
		$this->distinct 	= false;
		$this->insert 		= array();
        $this->onDuplicate  = array();
		$this->update 		= array();
		$this->from 		= array();
		$this->into 		= array();
		$this->join			= array();
		$this->where		= array();
		$this->group		= array();
		$this->having		= array();
		$this->order		= false;
		$this->limit		= false;
	}


	/**
	 * Gets the ID of the last inserted record
	 *
	 * @return numeric
	 */
	public function lastInsertID() {
		try {
			return $this->adapter->lastInsertID();
		} catch (DBU_Db_Exception $e) {
			$e->log();
		}
	}


	/**
	 * Gets the number of rows affected by the last operation
	 *
	 * @return numeric
	 */
	public function affectedRows() {
		try {
			return $this->adapter->affectedRows();
		} catch (DBU_Db_Exception $e) {
			$e->log();
		}
	}


	/**
	 * Disconnects from server
	 *
	 */
	public function disconnect() {
		if ($this->connection) {
			try {
				$this->adapter->disconnect();
			} catch (DBU_Db_Exception $e) {
				$e->log();
			}
		}
	}


	public function listTables() {
        return $this->adapter->listTables();
	}

	public function describeTable($table) {
		try {
			return $this->adapter->describeTable($this->protect($this->tableName($table)));
		} catch (DBU_Db_Exception $e) {
			$e->log();
		}
	}


    public function getQuery() {
        return $this->query;
    }


	/**
	 * Fetches singleton instance
	 *
	 * @param string $adapter
	 * @param string $db_prefix
	 * @return DBU_Db_Wrapper
	 */
	static public function instance($adapter = false, $db_prefix = false) {
		static $instance;
		if (!($instance instanceof self)) {
			$instance = new self($adapter, $db_prefix);
		}
		return $instance;
	}
}